msg_tool\scripts\ex_hibit\arc/
grp.rs1use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use anyhow::{Context, Result};
6use std::fmt::Debug;
7use std::io::{Read, Seek, SeekFrom};
8use std::path::{Path, PathBuf};
9use std::sync::{Arc, Mutex};
10
11#[derive(Debug)]
12pub struct ExHibitGrpArchiveBuilder {}
14
15impl ExHibitGrpArchiveBuilder {
16 pub const fn new() -> Self {
18 Self {}
19 }
20
21 fn build_with_reader<T>(
22 &self,
23 reader: T,
24 filename: &str,
25 archive_encoding: Encoding,
26 config: &ExtraConfig,
27 ) -> Result<Box<dyn Script>>
28 where
29 T: Read + Seek + Debug + 'static,
30 {
31 Ok(Box::new(ExHibitGrpArchive::new(
32 reader,
33 filename,
34 archive_encoding,
35 config,
36 )?))
37 }
38}
39
40impl ScriptBuilder for ExHibitGrpArchiveBuilder {
41 fn default_encoding(&self) -> Encoding {
42 Encoding::Cp932
43 }
44
45 fn default_archive_encoding(&self) -> Option<Encoding> {
46 Some(Encoding::Cp932)
47 }
48
49 fn build_script(
50 &self,
51 data: Vec<u8>,
52 filename: &str,
53 _encoding: Encoding,
54 archive_encoding: Encoding,
55 config: &ExtraConfig,
56 _archive: Option<&Box<dyn Script>>,
57 ) -> Result<Box<dyn Script>> {
58 self.build_with_reader(MemReader::new(data), filename, archive_encoding, config)
59 }
60
61 fn build_script_from_file(
62 &self,
63 filename: &str,
64 _encoding: Encoding,
65 archive_encoding: Encoding,
66 config: &ExtraConfig,
67 _archive: Option<&Box<dyn Script>>,
68 ) -> Result<Box<dyn Script>> {
69 if filename == "-" {
70 return Err(anyhow::anyhow!(
71 "Reading ExHibit GRP from stdin is not supported; provide a file path."
72 ));
73 }
74 let file = std::fs::File::open(filename)
75 .with_context(|| format!("Failed to open '{}'.", filename))?;
76 let reader = std::io::BufReader::new(file);
77 self.build_with_reader(reader, filename, archive_encoding, config)
78 }
79
80 fn build_script_from_reader(
81 &self,
82 reader: Box<dyn ReadSeek>,
83 filename: &str,
84 _encoding: Encoding,
85 archive_encoding: Encoding,
86 config: &ExtraConfig,
87 _archive: Option<&Box<dyn Script>>,
88 ) -> Result<Box<dyn Script>> {
89 self.build_with_reader(reader, filename, archive_encoding, config)
90 }
91
92 fn extensions(&self) -> &'static [&'static str] {
93 &["grp"]
94 }
95
96 fn script_type(&self) -> &'static ScriptType {
97 &ScriptType::ExHibitGrp
98 }
99
100 fn is_this_format(&self, filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
101 if !matches_grp_name(filename) {
102 return None;
103 }
104 if buf_len >= 4 && buf.starts_with(b"AiFS") {
105 return None;
106 }
107 Some(10)
108 }
109
110 fn is_archive(&self) -> bool {
111 true
112 }
113}
114
115#[derive(Clone, Debug)]
116struct GrpFileEntry {
117 name: String,
118 offset: u64,
119 size: u64,
120}
121
122#[derive(Debug)]
123pub struct ExHibitGrpArchive<T: Read + Seek + Debug> {
125 reader: Arc<Mutex<T>>,
126 entries: Vec<GrpFileEntry>,
127}
128
129impl<T: Read + Seek + Debug> ExHibitGrpArchive<T> {
130 fn new(
131 mut reader: T,
132 filename: &str,
133 _archive_encoding: Encoding,
134 _config: &ExtraConfig,
135 ) -> Result<Self> {
136 let mut header = [0u8; 4];
137 reader
138 .peek_exact_at(0, &mut header)
139 .context("Failed to read GRP header.")?;
140 if &header == b"AiFS" {
141 return Err(anyhow::anyhow!(
142 "Input file is a TOC (AiFS) rather than an archive."
143 ));
144 }
145
146 let path = Path::new(filename);
147 let (toc_path, arc_index) = locate_toc_file(path).context("Failed to locate TOC file.")?;
148
149 let archive_size = (&mut reader)
150 .stream_length()
151 .context("Failed to determine archive size.")?;
152
153 let entries = parse_toc_entries(&toc_path, arc_index, archive_size)
154 .with_context(|| format!("Failed to parse TOC '{}'.", toc_path.display()))?;
155
156 Ok(Self {
157 reader: Arc::new(Mutex::new(reader)),
158 entries,
159 })
160 }
161}
162
163impl<T: Read + Seek + Debug + 'static> Script for ExHibitGrpArchive<T> {
164 fn default_output_script_type(&self) -> OutputScriptType {
165 OutputScriptType::Json
166 }
167
168 fn default_format_type(&self) -> FormatOptions {
169 FormatOptions::None
170 }
171
172 fn is_archive(&self) -> bool {
173 true
174 }
175
176 fn iter_archive_filename<'a>(
177 &'a self,
178 ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
179 Ok(Box::new(
180 self.entries.iter().map(|entry| Ok(entry.name.clone())),
181 ))
182 }
183
184 fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
185 Ok(Box::new(self.entries.iter().map(|entry| Ok(entry.offset))))
186 }
187
188 fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + 'a>> {
189 if index >= self.entries.len() {
190 return Err(anyhow::anyhow!(
191 "Index out of bounds: {} (max: {}).",
192 index,
193 self.entries.len()
194 ));
195 }
196 let entry = self.entries[index].clone();
197 Ok(Box::new(GrpEntry::new(entry, self.reader.clone())))
198 }
199}
200
201struct GrpEntry<T: Read + Seek> {
202 info: GrpFileEntry,
203 reader: Arc<Mutex<T>>,
204 pos: u64,
205}
206
207impl<T: Read + Seek> GrpEntry<T> {
208 fn new(info: GrpFileEntry, reader: Arc<Mutex<T>>) -> Self {
209 Self {
210 info,
211 reader,
212 pos: 0,
213 }
214 }
215
216 fn remaining(&self) -> u64 {
217 self.info.size.saturating_sub(self.pos)
218 }
219}
220
221impl<T: Read + Seek> ArchiveContent for GrpEntry<T> {
222 fn name(&self) -> &str {
223 &self.info.name
224 }
225}
226
227impl<T: Read + Seek> Read for GrpEntry<T> {
228 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
229 if buf.is_empty() || self.pos >= self.info.size {
230 return Ok(0);
231 }
232 let remaining = self.remaining() as usize;
233 if remaining == 0 {
234 return Ok(0);
235 }
236 let to_read = buf.len().min(remaining);
237 let mut reader = self.reader.lock().map_err(|e| {
238 std::io::Error::new(
239 std::io::ErrorKind::Other,
240 format!("Failed to lock reader mutex: {}", e),
241 )
242 })?;
243 reader.seek(SeekFrom::Start(self.info.offset + self.pos))?;
244 let bytes = reader.read(&mut buf[..to_read])?;
245 self.pos = self.pos.checked_add(bytes as u64).ok_or_else(|| {
246 std::io::Error::new(std::io::ErrorKind::Other, "Read position overflow.")
247 })?;
248 Ok(bytes)
249 }
250}
251
252impl<T: Read + Seek> Seek for GrpEntry<T> {
253 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
254 let new_pos = match pos {
255 SeekFrom::Start(offset) => offset,
256 SeekFrom::End(offset) => {
257 let signed = self.info.size as i128 + offset as i128;
258 if signed < 0 {
259 return Err(std::io::Error::new(
260 std::io::ErrorKind::InvalidInput,
261 "Seek before entry start is not allowed.",
262 ));
263 }
264 signed as u64
265 }
266 SeekFrom::Current(offset) => {
267 let signed = self.pos as i128 + offset as i128;
268 if signed < 0 {
269 return Err(std::io::Error::new(
270 std::io::ErrorKind::InvalidInput,
271 "Seek before entry start is not allowed.",
272 ));
273 }
274 signed as u64
275 }
276 };
277 if new_pos > self.info.size {
278 return Err(std::io::Error::new(
279 std::io::ErrorKind::InvalidInput,
280 "Seek beyond entry size is not allowed.",
281 ));
282 }
283 self.pos = new_pos;
284 Ok(self.pos)
285 }
286}
287
288#[derive(Debug)]
289struct NameInfo {
290 digits_offset: usize,
291 digits_len: usize,
292 arc_num: u32,
293}
294
295fn matches_grp_name(filename: &str) -> bool {
296 Path::new(filename)
297 .file_name()
298 .and_then(|name| name.to_str())
299 .and_then(|name| parse_name_info(name).ok())
300 .is_some()
301}
302
303fn parse_name_info(name: &str) -> Result<NameInfo> {
304 if name.len() < 7 {
305 return Err(anyhow::anyhow!(
306 "Filename '{}' is too short for GRP pattern.",
307 name
308 ));
309 }
310 let prefix = &name[..3];
311 if !prefix.eq_ignore_ascii_case("res") {
312 return Err(anyhow::anyhow!(
313 "Filename '{}' does not start with 'res'.",
314 name
315 ));
316 }
317 let suffix = &name[name.len() - 4..];
318 if !suffix.eq_ignore_ascii_case(".grp") {
319 return Err(anyhow::anyhow!(
320 "Filename '{}' does not end with '.grp'.",
321 name
322 ));
323 }
324 let digits = &name[3..name.len() - 4];
325 if digits.is_empty() || !digits.chars().all(|c| c.is_ascii_digit()) {
326 return Err(anyhow::anyhow!(
327 "Filename '{}' does not contain a numeric sequence.",
328 name
329 ));
330 }
331 let arc_num = digits.parse::<u32>().with_context(|| {
332 format!(
333 "Failed to parse archive number from '{}' (digits '{}').",
334 name, digits
335 )
336 })?;
337 Ok(NameInfo {
338 digits_offset: 3,
339 digits_len: digits.len(),
340 arc_num,
341 })
342}
343
344fn locate_toc_file(path: &Path) -> Result<(PathBuf, u32)> {
345 let file_name = path
346 .file_name()
347 .and_then(|name| name.to_str())
348 .ok_or_else(|| anyhow::anyhow!("Filename contains invalid UTF-8."))?;
349 let info = parse_name_info(file_name)?;
350 if info.arc_num == 0 {
351 return Err(anyhow::anyhow!(
352 "Archive '{}' has number 0 and therefore no preceding TOC file.",
353 file_name
354 ));
355 }
356
357 let mut toc_num = info.arc_num as i64 - 1;
358 let mut arc_index: u32 = 1;
359 while toc_num >= 0 {
360 let digits = format!("{:0width$}", toc_num, width = info.digits_len);
361 let mut candidate = String::with_capacity(file_name.len());
362 candidate.push_str(&file_name[..info.digits_offset]);
363 candidate.push_str(&digits);
364 candidate.push_str(&file_name[info.digits_offset + info.digits_len..]);
365 let candidate_path = path.with_file_name(&candidate);
366 if !candidate_path.exists() {
367 return Err(anyhow::anyhow!(
368 "TOC file '{}' does not exist.",
369 candidate_path.display()
370 ));
371 }
372 let mut file = std::fs::File::open(&candidate_path).with_context(|| {
373 format!(
374 "Failed to open TOC candidate '{}'.",
375 candidate_path.display()
376 )
377 })?;
378 let mut header = [0u8; 4];
379 file.read_exact(&mut header).with_context(|| {
380 format!("Failed to read header from '{}'.", candidate_path.display())
381 })?;
382 if &header == b"AiFS" {
383 return Ok((candidate_path, arc_index));
384 }
385 toc_num -= 1;
386 arc_index = arc_index
387 .checked_add(1)
388 .ok_or_else(|| anyhow::anyhow!("Archive index overflow while searching TOC."))?;
389 }
390
391 Err(anyhow::anyhow!(
392 "Unable to locate a TOC (AiFS) file for '{}'.",
393 file_name
394 ))
395}
396
397fn parse_toc_entries(
398 toc_path: &Path,
399 arc_index: u32,
400 archive_size: u64,
401) -> Result<Vec<GrpFileEntry>> {
402 let file = std::fs::File::open(toc_path)?;
403 let mut reader = std::io::BufReader::new(file);
404 let toc_len = reader.stream_length()?;
405 if toc_len < 0x10 {
406 return Err(anyhow::anyhow!("TOC file is too small."));
407 }
408
409 reader.seek(SeekFrom::Start(0xC))?;
410 let res_count = reader.read_i32()?;
411 if res_count <= 0 {
412 return Err(anyhow::anyhow!("TOC resource count is invalid."));
413 }
414 if arc_index as i64 > res_count as i64 {
415 return Err(anyhow::anyhow!(
416 "Archive index {} is out of range (resource count {}).",
417 arc_index,
418 res_count
419 ));
420 }
421
422 let mut index_offset = 0x10u64;
423 let mut arc_offset = None;
424 for _ in 0..res_count {
425 if index_offset + 0x10 > toc_len {
426 break;
427 }
428 reader.seek(SeekFrom::Start(index_offset))?;
429 let mut num = reader.read_i32()?;
430 if num == 0x0100_0000 {
431 index_offset = index_offset
432 .checked_add(4)
433 .ok_or_else(|| anyhow::anyhow!("Index offset overflow."))?;
434 if index_offset + 4 > toc_len {
435 break;
436 }
437 reader.seek(SeekFrom::Start(index_offset))?;
438 num = reader.read_i32()?;
439 }
440 reader.seek(SeekFrom::Start(index_offset + 0xC))?;
441 let entry_count = reader.read_u32()?;
442 if num == arc_index as i32 {
443 arc_offset = Some(index_offset);
444 break;
445 }
446 let step = (entry_count as u64)
447 .checked_mul(8)
448 .and_then(|v| v.checked_add(0x10))
449 .ok_or_else(|| anyhow::anyhow!("Index offset overflow while skipping entries."))?;
450 index_offset = index_offset
451 .checked_add(step)
452 .ok_or_else(|| anyhow::anyhow!("Index offset overflow while iterating."))?;
453 }
454
455 let arc_offset =
456 arc_offset.ok_or_else(|| anyhow::anyhow!("Archive reference not found in TOC."))?;
457
458 reader.seek(SeekFrom::Start(arc_offset + 4))?;
459 let start_index = reader.read_i32()?;
460 if start_index < 0 {
461 return Err(anyhow::anyhow!("Start index is negative."));
462 }
463 reader.seek(SeekFrom::Start(arc_offset + 0xC))?;
464 let entry_count = reader.read_i32()?;
465 if entry_count < 0 {
466 return Err(anyhow::anyhow!("Entry count is negative."));
467 }
468 let entry_count = entry_count as u32;
469
470 let data_offset = arc_offset
471 .checked_add(0x10)
472 .ok_or_else(|| anyhow::anyhow!("Entry table offset overflow."))?;
473 let table_len = (entry_count as u64)
474 .checked_mul(8)
475 .ok_or_else(|| anyhow::anyhow!("Entry table size overflow."))?;
476 if data_offset + table_len > toc_len {
477 return Err(anyhow::anyhow!("TOC entry table exceeds file size."));
478 }
479
480 let mut entries = Vec::with_capacity(entry_count as usize);
481 let mut entry_offset = data_offset;
482 for i in 0..entry_count {
483 reader.seek(SeekFrom::Start(entry_offset))?;
484 let offset = reader.read_u32()? as u64;
485 let size = reader.read_u32()? as u64;
486 if size != 0 {
487 let end = offset
488 .checked_add(size)
489 .ok_or_else(|| anyhow::anyhow!("Entry size overflow."))?;
490 if end > archive_size {
491 return Err(anyhow::anyhow!(
492 "Entry {} exceeds archive size (offset {}, size {}).",
493 i,
494 offset,
495 size
496 ));
497 }
498 let index = (start_index as u32)
499 .checked_add(i)
500 .ok_or_else(|| anyhow::anyhow!("Entry index overflow."))?;
501 entries.push(GrpFileEntry {
502 name: format!("{:05}.ogg", index),
503 offset,
504 size,
505 });
506 }
507 entry_offset += 8;
508 }
509
510 if entries.is_empty() {
511 return Err(anyhow::anyhow!("Archive contains no entries."));
512 }
513
514 Ok(entries)
515}